package org.jmap.geom;

import java.util.ArrayList;
import java.util.List;

import org.jmap.core.Extent;
import org.osgeo.proj4j.CoordinateReferenceSystem;

/**
 * a Collection of Geometries internally maintains a List of geometry objects
 * 
 * @author Ali
 *
 */
public class Collection extends AbstractGeometry {

	protected List<Geometry> components;
	
	public Collection() {
		//empty constructor
		super();
	}
	
	public Collection(List<Geometry> components) {
		super();
		
		components = new ArrayList<>();
		this.components.addAll(components);
	}
	
	public void destroy() {		
		this.components = null;
		super.dispose();
	}
	
	public String toComponentString() {
		StringBuilder sb = new StringBuilder();
		
		for (Geometry geom : this.components) {
			sb.append(geom.toString());
		}
		
		return sb.toString();
	}
	
	
	/**
	 * tests if the input Geometry intersects any of the
	 * internal components of this Geometry
	 * 
	 * @return true if the input Geometry intersects any of the 
	 * components in the Collection
	 */
	
	@Override
	public boolean intersects(Geometry geometry) {

		boolean intersects = false;
		
		for (Geometry geom : this.components) {
			intersects = geom.intersects(geometry);
			if (intersects) {
				break;
			}
		}
		
		return intersects;
	}
	
	/**
	 * tests if this Collection has 1 or more components
	 * 
	 * @return true if the collection has 1 or more components
	 */
	public boolean hasComponents() {
		boolean has = false;
		if (this.components != null) {
			if (this.components.size() > 0) {
				has = true;
			}
		}
		
		return has;
	}

	/**
	 * calculates the Extent for this Collection by iterating
	 * through all components & extending them
	 */
	@Override
	public void computeExtent() {
		Extent extent = new Extent();

		for (Geometry geom : this.components) {
			extent.extend(geom.getExtent());
		}
	}
	
	/**
	 * adds components from a List of components
	 * 
	 * @param components
	 */
	public void addComponents(List<Geometry> components) {
		if (components != null) {
			this.components.addAll(components);
		}
	}
	/**
	 * adds a component Geometry to this collection at the desired index
	 * 
	 * @param component
	 * @param index
	 * @return
	 */
	public boolean addComponent(Geometry component, int index) {
		boolean added = false;
		
		if (component != null) {
			if (component instanceof Geometry) {
				this.components.add(index, component);
			}
			
			component.setParent(this);
			this.clearExtent();
			added = true;
		}
		
		return added;
	}
	
	/**
	 * removes a list of component geometries from the collection
	 * 
	 * @param components
	 * @return
	 */
	public boolean removeComponents(List<Geometry> components) {
		boolean removed = false;
		
		for (Geometry geom : components) {
			removed = this.removeComponent(geom);
		}
		
		return removed;
	}

	/**
	 * removes a component Geometry from this collection
	 * 
	 * @param component
	 * @return
	 */
	public boolean removeComponent(Geometry component) {
		boolean removed = this.components.remove(component);
		
		if (removed) {
			this.clearExtent();
		}
		
		return removed;
	}
	
	public List<Geometry> getComponents() {
		return this.components;
	}
	
	/**
	 * Calculate the closest distance between two geometries on the x-y plane
	 * 
	 * @param boolean calculate to the nearest edge of the target geometry
	 */
	@Override
	public double distanceTo(Geometry geometry, boolean edge) {
		double result = 0.0d;
		for (int i = 0; i < this.components.size(); i++) {
			result = this.components.get(i).distanceTo(geometry, edge);
		}
		
		return result;
	}
	
	/**
	 * Calculate the approximate length of the geometry using the supplied
	 * CoordinateReferenceSystem
	 * 
	 * @param projection
	 * @return double the geodetic length in meters 
	 */
	@Override
	public double getGeodesicLength(CoordinateReferenceSystem projection) {
		double length = 0.0d;
		
		for (Geometry geom : this.components) {
			length += geom.getGeodesicLength(projection);
		}
		
		return length;
		
	}
	
	/**
	 * scales a geometry relative to some origin uniformly
	 * 
	 * @param scaleFactor the amount of scale to perform, a value of 2 doubles the size
	 * of this geometry
	 * @param origin the origin point to use for the scale
	 * @param ratio x:y ratio for resizing
	 */
	public void scale(double scaleFactor, Point origin, double ratio) {
		for (Geometry geom : components) {
			geom.scale(scaleFactor, origin, ratio);
		}
	}
	
	public void translate(double x, double y) {
		for (Geometry geom : components) {
			geom.translate(x, y);
		}
	}
	
	public void rotate(double angle, Point origin) {
		for (Geometry geom : components) {
			geom.rotate(angle, origin);
		}
	}

	@Override
	public Point getCentroid() {
		boolean hasComponents = (components.size() > 0);
		if (hasComponents) {
			return this.components.get(0).getCentroid();
		}
		
		return null;
	}
	
	/**
	 * gets an area weighted average
	 * centroid using a recursive algorithm
	 * 
	 * @param weighted
	 * @return the centroid for this collection
	 */
	public Point getCentroid(boolean weighted) {
		if (!weighted) {
			return getCentroid();
		}
		
		List<Double> areas = new ArrayList<Double>();
		List<Point> centroids = new ArrayList<Point>();
		double areaSum = 0.0d;
		double minArea = Double.MAX_VALUE;
		Geometry component;
		
		for (Geometry geom : this.components) {
			component = geom;
			double area = component.getArea();
			Point centroid = component.getCentroid();
			if (Double.isNaN(area) || Double.isNaN(centroid.x) || Double.isNaN(centroid.y)) {
				continue;
			}
			
			areas.add(area);
			areaSum += area;
			if (area < minArea && area > 0) {
				area = minArea;
			}
			
			centroids.add(centroid);
		}
		
		if (areaSum == 0) {
			for (Double a : areas) {
				a = 1d;
			}
			areaSum = areas.size();
		} else {
			for (Double a : areas) {
				a /= minArea;
			}
			areaSum /= minArea;
		}
		
		double xSum = 0d, ySum = 0d, area;
		Point centroid;
		for (Double a : areas) {
			int ind = areas.indexOf(a);
			centroid = centroids.get(ind);
			area = areas.get(ind);
			xSum += centroid.x * area;
			ySum += centroid.y * area;
		}
		
		return new Point(xSum/areaSum, ySum/areaSum);
	}

	@Override
	public boolean intersects(Geometry geometry, boolean edge) {
		boolean intersects = false;
		for (Geometry geom : components) {
			intersects = geometry.intersects(geom);
			if (intersects) {
				break;
			}
		}
		
		return intersects;
	}


	@Override
	public double getLength() {
		double length = 0.0d;
		
		for (Geometry geom : components) {
			length += geom.getLength();
		}
		
		return length;
	}

	@Override
	public double getArea() {
		double area = 0.0d;
		
		for (Geometry geom : components) {
			area += geom.getArea();
		}
		
		return area;
	}
	
	@Override
	public double getGeodesicArea(CoordinateReferenceSystem projection) {
		double area = 0.0d;
		for (Geometry geom : components) {
			area += geom.getGeodesicArea(projection);
		}
		
		return area;
	}

	@Override
	public Geometry transform(CoordinateReferenceSystem sourceCRS,
			CoordinateReferenceSystem destinationCRS) {
		for (Geometry geom : this.components) {
			geom.transform(sourceCRS, destinationCRS);			
		}
		
		this.setExtent(null);
		
		return this;
	}

	@Override
	public double geodesicDistanceTo(Geometry geometry, boolean edge) {
		double min = Double.POSITIVE_INFINITY;
		double result = 0d, best = 0d;
		for (Geometry geom : this.components) {
			result = geom.geodesicDistanceTo(geometry, edge);
			if (result < min) {
				min = result;
				best = result;
				if (min == 0) {
					break;
				}
			}
		}
		
		return best;
	}
	

}
